package cloudspace.vm;

import java.io.File;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javassist.ClassPool;
import javassist.Loader;
import javassist.NotFoundException;

import org.apache.log4j.Logger;
import org.zkoss.zk.ui.Executions;

import student.web.internal.ApplicationSupportStrategy;
import student.web.internal.LocalityService;
import bsh.Interpreter;
import cloudspace.vm.filesystem.ProjectSpec;
import cloudspace.vm.io.console.InfoStream;
import cloudspace.vm.io.console.SystemStreamMultiplexer;
import cloudspace.vm.javassist.JavassistBootloader;
import cloudspace.vm.javassist.MalformedConfigurationException;


// TODO: implement a strategy for handling memory management
/**
 * The Class VM. This is a basic Cloudspace Virtual Machine. This virtual
 * Machine provides shared information about the specific virtual machine to
 * every virtual machine context created in this context. This virtual machine
 * holds information about. local classes, local loader, delegated classes,
 * standard output streams, and parent loaders. There is one of these per
 * instance of a virtual machine.
 */
public class VM
{
    private Map<String, Object> localObjects = new HashMap<String, Object>();


    /**
     * An exception that occurs when a thread requests a VM and there is no vm
     * tagged in the threadLocal.
     * 
     * @author mjw87
     * 
     */
    public static class NoVmException extends RuntimeException
    {

        /**
         *
         */
        private static final long serialVersionUID = 524965398230936313L;


        public NoVmException( String msg )
        {
            super( msg );
        }

    }

    protected static Logger logger = Logger.getLogger( VM.class );


    // -------------------------------------------------------------------------
    /**
     * The Class LoggedClassPool. This class represents a Logged class pool that
     * can detect updates to the class files stored internally.
     */
    private class LoggedClassPool extends ClassPool
    {
        List<File> currentClassPath;

        /** The logged class path entries. */
        private long timeStamp;


        public LoggedClassPool( ClassPool parent )
        {
            super( parent );
            init();
        }


        /**
         * Initializes the class pool's variables.
         */
        private void init()
        {
            this.logUpdate();
        }


        /**
         * This private method recurses through the classpath entries to log
         * time stamps for every file in the class path.
         * 
         * @param classPathDir
         *            the class path directory to recurse and log.
         */
        private List<File> recurseClassPathEntry( File root )
        {
            List<File> flattenList = new ArrayList<File>();
            File[] dirList = root.listFiles();
            if ( dirList != null )
            {
                for ( File classPathEntry : dirList )
                {
                    if ( classPathEntry.isDirectory() )
                    {
                        flattenList.addAll( recurseClassPathEntry( classPathEntry ) );
                    }
                    else
                    {
                        flattenList.add( classPathEntry );
                    }
                }
            }
            return flattenList;
        }


        /**
         * Checks if the class pool has been updated
         * 
         * @return true, if is updated
         */
        public boolean isUpdated()
        {
            for ( File oldFiles : currentClassPath )
            {
                if ( !oldFiles.exists() )
                    return true;
            }
            List<File> flatList = recurseClassPathEntry( localRoot );
            for ( File lFile : flatList )
            {
                if ( lFile.lastModified() > timeStamp )
                {
                    return true;
                }
            }
            return false;
        }


        public void logUpdate()
        {
            timeStamp = java.util.Calendar.getInstance().getTime().getTime();
            currentClassPath = this.recurseClassPathEntry( localRoot );
        }
    }

    /**
     * This set keeps track of all of the desktops associated with this virtual
     * machine.
     */
    // private Set<String> desktops;
    /**
     * The local root directory for the virtual machine.
     */
    private File localRoot;


    /**
     * The Enum PrinterType.
     */
    public enum PrinterType {

        /** The OUT. */
        OUT,
        /** The ERROR. */
        ERROR
    };

    private String loaderStamp;

    /**
     * The protected Info Stream tags for VM provided streams.
     */
    public static final String standardOutTag = "Standard Out";

    public static final String standardErrorTag = "Standard Error";

    public static final String logTag = "CloudSpace Logs";

    /**
     * The Standard Out stream and Printer. The printer is pre wrapped just for
     * convenience this allows a user to perform VM.out to print to the VM
     * specfic standard out
     */
    private final InfoStream stdOut = new InfoStream( standardOutTag );

    public final PrintStream out = new PrintStream( stdOut );

    /** See above for reasoning */
    private final InfoStream stdErr = new InfoStream( standardErrorTag );

    public final PrintStream err = new PrintStream( stdErr );

    private final InfoStream stdLog = new InfoStream( logTag );

    public final PrintStream log = new PrintStream( stdLog );

    private Lock eventLock = new ReentrantLock();

    /** The mapping from local Directories (unique to each VM) to cloudspaceVMs. */
    private static Map<String, VM> directoryToVM = new HashMap<String, VM>();

    /**
     * The current loader for the VM. This is updated when classes have been
     * changed in the classpool.
     */
    private Loader vmLoader;

    private LoggedClassPool vmPool;

    /** The thread current vm. */
    private static ThreadLocal<VM> threadCurrentVM = new ThreadLocal<VM>();

    private ThreadLocal<Boolean> threadHasLock = new ThreadLocal<Boolean>()
    {
        protected Boolean initialValue()
        {
            return false;
        }
    };


    public boolean hasVM()
    {
        return threadHasLock.get();
    }


    /**
     * Enter this VM.
     */
    public void lockVM()
    {
        eventLock.lock();
        threadHasLock.set( true );
    }


    /**
     * This silently sets the currentVM it will not take ownership of the VM!
     * BECAREFUL!!
     * 
     */
    public void enter()
    {
        enter0();

    }


    /**
     * Enter this VM and associate desktop with id 'dID' with the VM.
     */
    private void enter0()
    {
        threadCurrentVM.set( this );
    }


    /**
     * Leave this VM.
     */
    public void unlockVM()
    {
        threadHasLock.set( false );
        eventLock.unlock();
    }


    /**
     * This silently leaves the VM's context. BECAREFUL!
     * 
     */
    public static void leave()
    {
        leave0();
    }


    private static void leave0()
    {
        threadCurrentVM.set( null );
    }


    /**
     * Gets the current VM.
     * 
     * @return the current VM
     */
    public static VM currentVM()
    {
        try
        {
            return threadCurrentVM.get();
        }
        catch ( NullPointerException e )
        {
            throw new NoVmException( "There is no VM associated with this thread" );
        }
    }


    /**
     * This method looks up the current VM context a class is acting in. It uses
     * ZK desktop and page information to determine which vm instance a thread
     * is acting in.
     * 
     * Will not enter VM.
     * 
     * @return the instance of the vm associated with the current desktop's
     *         basedir
     */
    public static VM lookupVM()
    {
        String currentPage = Executions.getCurrent()
            .getDesktop()
            .getRequestPath();
        String currentDir = getBaseDir( currentPage );
        String localRoot = Executions.getCurrent()
            .getDesktop()
            .getWebApp()
            .getRealPath( currentDir );
        return lookupVM( localRoot );
    }


    /**
     * Look up VM associated with 'baseDir', creating it if necessary.
     */
    public static VM lookupVM( String baseDir )
    {
        String projectBaseDir = ProjectSpec.getBaseProjectDir( baseDir );
        VM vm = directoryToVM.get( projectBaseDir );
        logger.info( "Looking up " + baseDir + " as " + projectBaseDir );
        if ( vm == null )
        {
            vm = new VM( projectBaseDir );

            vm.addEventListener( new VM.EventListenerAdapter()
            {

                @Override
                public void onClassReload()
                {
                    /*
                     * We must reset session attributes if classes are reloaded.
                     * Determine if 'app-store' is sufficient or not.
                     */
                    ApplicationSupportStrategy strat = LocalityService.getSupportStrategy();
                    strat.removeSessionParameter( "app-store" );
                    strat.removeSessionParameter( "context-object" );
                }

            } );
            directoryToVM.put( projectBaseDir, vm );
        }
        return vm;
    }


    /**
     * This private member requests an update to the Class Loader registered to
     * this virtual Machine. The update will only happen if a class in the class
     * pool has been updated.
     */
    private void updateLoader()
    {
        synchronized ( vmPool )
        {
            if ( vmPool.isUpdated() )
            {
                logger.info( "Updating classes." );
                updateLoader0();

            }
        }

    }


    /**
     * Gets the base dir of a page. This method assumes a
     * www.webpage.domain/<UNIQUE PAGE TAG>/...... If the format differs from
     * this, cloudspace will not work.
     * 
     * @param currentPage
     *            string representing the current request path.
     * 
     * @return the base dir representation of a page request.
     */
    private static String getBaseDir( String currentPage )
    {
        int index = currentPage.indexOf( '/' );
        int index2 = currentPage.lastIndexOf( "/" );
        String callPath = currentPage.substring( index, index2 );
        return callPath;
    }


    /**
     * Instantiates a new cloudspace vm. Using a localRoot and desktop ID.
     * 
     * @param vmContext
     *            the desk id
     * @param localRoot
     *            the local root
     */
    public VM( String projectBaseDir )
    {
        // desktops = new HashSet<String>();
        localRoot = new File( projectBaseDir );
        logger.info( "Creating new Virtual Machine for " + localRoot.getPath()
            + " on request path " + projectBaseDir );
        updateLoader0();
    }


    private void updateLoader0()
    {
        this.log.println( "Source code has changed, server has been updated!" );
        final ClassLoader parentLoader = this.getClass().getClassLoader();
        ClassPool proxyPool = JavassistBootloader.getProxyPool();
        vmPool = new LoggedClassPool( proxyPool );

        /* define locations in which rewritable .class files are */
        try
        {
            if ( Executions.getCurrent() != null )
            {
                logger.info( "(" + localRoot.getPath()
                    + ") Adding /WEB-INF/pagelib/ to classpath" );
                vmPool.appendClassPath( Executions.getCurrent()
                    .getDesktop()
                    .getWebApp()
                    .getRealPath( "/WEB-INF/pagelib/" )
                    + "*" );
            }
            logger.info( "(" + localRoot.getPath()
                + ") Adding local directory to classpath" );
            vmPool.appendClassPath( localRoot.getAbsolutePath() );
            logger.info( "(" + localRoot.getPath()
                + ") Adding local directory jars to classpath" );
            vmPool.appendClassPath( localRoot + File.separator + "*" );
            logger.info( "("
                + localRoot.getPath()
                + ") Adding classes directory within the local directory to classpath" );
            vmPool.appendClassPath( localRoot + File.separator + "classes"
                + File.separator + "*" );
            logger.info( "(" + localRoot.getPath()
                + ") Adding System classpath to classpath" );
            vmPool.appendSystemPath();
        }
        catch ( NotFoundException e )
        {
            logger.error( "Class pool could not initialize because of missing directories",
                e );
        }
        vmPool.logUpdate();

        /* always attempt to delegate to parent first */
        vmLoader = new Loader( parentLoader, vmPool )
        {
            protected Class<?> loadClassByDelegation( String clazzName )
                throws ClassNotFoundException
            {
                if ( clazzName.startsWith( "_test." ) )
                {
                    return null;
                }
                try
                {
                    return delegateToParent( clazzName );
                }
                catch ( ClassNotFoundException ncdef )
                {
                    return null;
                }
            }
        };
        loaderStamp = Calendar.getInstance().getTime().toString();
        try
        {
            JavassistBootloader.translateClasses( vmLoader, vmPool );
        }
        catch ( MalformedConfigurationException e )
        {
            logger.error( "Configuration problem in Javassist config", e );
        }
        runLoaderListeners();

    }


    /**
     * Prepares an interpreter for use in the current context.
     * 
     * @param ip
     */
    public void prepareInterpreter( Interpreter ip )
    {
        updateLoader();
        ip.setClassLoader( vmLoader );
    }


    /**
     * Gets the Info Stream operating as a backend to System.out.
     * 
     * @return InfoStream System.out
     */
    public InfoStream getStdOut()
    {
        return stdOut;
    }


    /**
     * Gets the Info Stream operating as a backend to System.err.
     * 
     * @return InfoStream System.err
     */
    public InfoStream getError()
    {
        return stdErr;
    }


    /**
     * This gets a VM hardwired stream according to a printer type. Currently,
     * only system.out and system.err are hardwired.
     * 
     * @param streamID
     * @return the protected VM stream corresponding to the passed printer type.
     */
    public PrintStream getStream( PrinterType streamID )
    {
        switch ( streamID )
        {
            case OUT:
                return out;
            case ERROR:
                return err;
            default:
                return null;
        }
    }


    /**
     * Get the unique local root for this VM. A VM only has one local root and
     * it does not share this local root with any other VM.
     * 
     * @return The root directory for the VM.
     */
    public String getLocalRoot()
    {
        return localRoot.getAbsolutePath();
    }


    /**
     * Gets the class pool for this VM
     * 
     * @return the VM's class pool
     */
    public ClassPool getClassPool()
    {
        return vmPool;
    }


    public boolean isLocalClass( final String classname )
    {
        return vmPool.find( classname ) != null
            || classname.startsWith( "_test.wrap" );
    }


    /**
     * Gets the class loader used for all bean shell interpreters in this VM's
     * Context
     * 
     * @return the current Context's class loader
     */
    public Loader getLoader()
    {
        return vmLoader;
    }

    // Static init block. This sets up the SystemStreamMultiplexer to start
    // intercepting system.out and system.err
    static
    {
        if ( !( System.out instanceof SystemStreamMultiplexer ) )
        {
            SystemStreamMultiplexer ssm = new SystemStreamMultiplexer( System.out );
            ssm.setStream( PrinterType.OUT );
            System.setOut( ssm );
        }
        if ( !( System.err instanceof SystemStreamMultiplexer ) )
        {
            SystemStreamMultiplexer ssmErr = new SystemStreamMultiplexer( System.err );
            ssmErr.setStream( PrinterType.ERROR );
            System.setErr( ssmErr );
        }
    }


    /**
     * Gets the Info Stream representing the CloudSpace Log.
     * 
     * @return the protected InfoStream Log
     */
    public InfoStream getLog()
    {
        return stdLog;
    }


    /**
     * Get the last time the loader was initialized.
     * 
     * @return the current Loader timestamp
     */
    public String getLoaderTime()
    {
        return loaderStamp;
    }


    /**
     * Registers a desktop to the current VM.
     * 
     * @param dID
     *            Desktop ID.
     */
    /*
     * public void addDesktop(String dID) { if(!desktops.contains(dID))
     * desktops.add(dID); }
     */

    /**
     * de registers a Desktop ID from the Virtual Machine.
     * 
     * @param dID
     *            the Desktop ID to remove.
     */
    /*
     * public void removeDesktop(String dID) { desktops.remove(dID); }
     */

    /**
     * Gets the number desktops registered to this VM.
     * 
     * @return the number desktops
     */
    /*
     * public int getNumberDesktops() { return desktops.size(); }
     */

    /**
     * A class listeners for VM events must implement.
     */
    public interface EventListener
    {
        /**
         * Called if VM reloads user classes.
         */
        public void onClassReload();

        /* Add others here. */
    }


    /**
     * An adapter class for VM.EventListener
     */
    public static abstract class EventListenerAdapter implements EventListener
    {
        public void onClassReload()
        {
        }
    }

    private List<EventListener> vmListeners = new ArrayList<EventListener>();


    /**
     * Add an VM event listener.
     */
    public void addEventListener( EventListener listener )
    {
        vmListeners.add( listener );
    }


    /**
     * Remove an VM event listener.
     */
    public void removeEventListener( EventListener listener )
    {
        vmListeners.remove( listener );
    }


    private void runLoaderListeners()
    {
        for ( EventListener l : vmListeners )
        {
            l.onClassReload();
        }
    }


    public void setLocalObject( String key, Object obj )
    {
        localObjects.put( key, obj );
    }


    public Object getLocalObject( String key )
    {
        return localObjects.get( key );
    }
}
